package lumis.content.core;

import lumis.content.ContentManagerFactory;
import lumis.content.workflow.ContentWorkflowMetaData;
import lumis.content.workflow.IWorkflowManager;
import lumis.doui.DouiManagerFactory;
import lumis.portal.PortalException;
import lumis.portal.PortalObjectAlreadyExistsException;
import lumis.portal.authentication.SessionConfig;
import lumis.portal.cache.PortalCache;
import lumis.portal.event.persistence.PortalEventEntityListener;
import lumis.portal.manager.ManagerFactory;
import lumis.portal.stability.StableMinor;
import lumis.portal.transaction.ITransaction;
import lumis.portal.transaction.PortalTransactionFactory;
import lumis.util.LocaleUtil;
import lumis.util.PortalUtil;
import lumis.util.XmlUtil;
import lumis.util.log.ILogger;
import lumis.util.log.LoggerFactory;

import java.io.Serializable;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Date;
import java.util.List;
import java.util.Locale;

import javax.persistence.CascadeType;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.EntityListeners;
import javax.persistence.EntityManager;
import javax.persistence.FetchType;
import javax.persistence.Id;
import javax.persistence.JoinColumn;
import javax.persistence.JoinTable;
import javax.persistence.ManyToMany;
import javax.persistence.ManyToOne;
import javax.persistence.NoResultException;
import javax.persistence.OneToMany;
import javax.persistence.OneToOne;
import javax.persistence.OrderBy;
import javax.persistence.PostPersist;
import javax.persistence.PrePersist;
import javax.persistence.PreRemove;
import javax.persistence.PreUpdate;
import javax.persistence.Table;
import javax.persistence.Transient;

import lumis.content.ContentManagerFactory;
import lumis.content.workflow.ContentWorkflowMetaData;
import lumis.content.workflow.IWorkflowManager;
import lumis.doui.DouiManagerFactory;
import lumis.portal.PortalException;
import lumis.portal.PortalObjectAlreadyExistsException;
import lumis.portal.authentication.SessionConfig;
import lumis.portal.cache.PortalCache;
import lumis.portal.event.persistence.PortalEventEntityListener;
import lumis.portal.manager.ManagerFactory;
import lumis.portal.serviceinstance.ServiceInstanceConfig;
import lumis.portal.serviceinstance.ServiceInstanceDependencyConfig;
import lumis.portal.stability.StableMinor;
import lumis.portal.transaction.ITransaction;
import lumis.portal.transaction.PortalTransactionFactory;
import lumis.util.LocaleUtil;
import lumis.util.PortalUtil;
import lumis.util.XmlUtil;
import lumis.util.log.ILogger;
import lumis.util.log.LoggerFactory;

import org.hibernate.Hibernate;
import org.hibernate.annotations.Cache;
import org.hibernate.annotations.CacheConcurrencyStrategy;
import org.hibernate.annotations.CollectionId;
import org.hibernate.annotations.GenericGenerator;
import org.hibernate.annotations.Parameter;
import org.hibernate.annotations.Type;
import org.w3c.dom.Node;

/**
 * Contains the content information for a specific locale.
 * There must be at least one {@link ContentVersion} in each instance of this
 * class.
 * 
 * @stereotype entity
 * @composed 1 - 1..* ContentVersion
 * @composed 1 - * ContentComment
 *
 * @version $Revision: 13092 $ $Date: 2011-05-28 18:19:06 -0300 (Sat, 28 May 2011) $
 * @since 4.2.0
 */
@StableMinor(version = "6.0", sinceVersion = "4.2")
@Entity
@org.hibernate.annotations.Entity(dynamicUpdate=true, dynamicInsert=true)
@Table(name="lum_ContentLocale")
@GenericGenerator(name="portal-guid", strategy="lumis.portal.dao.hibernate.GuidGenerator")
@Cache(usage = CacheConcurrencyStrategy.NONSTRICT_READ_WRITE)
@EntityListeners(value=PortalEventEntityListener.class)
@SuppressWarnings("serial")
public class ContentLocale implements Serializable
{
	private static final ILogger logger = LoggerFactory.getLogger(ContentLocale.class);

	@Id
	private String id;
	
	private String createdBy;
	
	private Date createdDateTime = new Date();
	
	private String locale;
	
	@SuppressWarnings("unused") // used only on queries
	private boolean dirty;

	@OneToOne(cascade={CascadeType.PERSIST, CascadeType.MERGE, CascadeType.REFRESH}, fetch=FetchType.LAZY)
	@JoinColumn(name="activeVersionId")
	private ContentVersion activeVersion;
	
	@OneToOne(cascade={CascadeType.PERSIST, CascadeType.MERGE, CascadeType.REFRESH}, fetch=FetchType.LAZY)
	@JoinColumn(name="publishedVersionId")
	private ContentVersion publishedVersion;
	
	@OneToMany(mappedBy="contentLocale", cascade=CascadeType.ALL)
	@OrderBy(value="createdDateTime")
	@Cache(usage=CacheConcurrencyStrategy.NONSTRICT_READ_WRITE)
	private List<ContentComment> comments = new ArrayList<ContentComment>();
	
	@OneToMany(mappedBy="contentLocale", cascade=CascadeType.ALL)
	@OrderBy(value="versionNumber")
	@Cache(usage=CacheConcurrencyStrategy.NONSTRICT_READ_WRITE)
	private List<ContentVersion> versions = new ArrayList<ContentVersion>();
	
	@ManyToOne(fetch=FetchType.EAGER)
	@JoinColumn(name="contentId")
	private Content content;
	
	/**
	 * A collection of tags associated to this content locale.
	 * @see #addTag(String)
	 * @see #getTags()
	 * @see #removeTag(ContentTag)
	 * @since 6.0.0
	 */
	@ManyToMany
	@JoinTable
	(
		name="lum_ContentLocaleTag",
		joinColumns={@JoinColumn(name="contentLocaleId")},
		inverseJoinColumns={@JoinColumn(name="tagContentId")}
	)
    @CollectionId
    (
        columns=@Column(name="id"), 
        type=@Type(type="string"),
        generator="portal-guid"
    )
	@Cache(usage = CacheConcurrencyStrategy.NONSTRICT_READ_WRITE)
	private Collection<Content> tags = new ArrayList<Content>();
	
	/**
	 * Indicates whether the current ContenLocale is locked and what type of lock it is. 
	 * @since 6.0.0
	 */
	@Column(name= "lockType", columnDefinition="string", nullable = true)	
	@Type(
		type = "lumis.portal.dao.hibernate.GenericEnumUserType",
		parameters = 
		{
			@Parameter(
				name  = "enumClass",
				value = "lumis.content.core.LockType"),
			@Parameter(
				name  = "identifierMethod",
				value = "name"),
			@Parameter(
				name  = "valueOfMethod",
				value = "valueOf")
		}
	)
	private LockType lockType;
	  	
	/**
	 * Indicates who is the current lock holder.
	 * @since 6.0.0
	 */
	private String lockedBy;
	
	/**
	 * Date of when the lock was created/acquired.
	 * @since 6.0.0
	 */
	private Date lockedDate;
	
	/**
	 * Indicates whether a {@link EntityManager#flush() flush} is required before
	 * loading the {@link #versions} collection.
	 * @since 6.0.0
	 */
	@Transient
	private transient boolean flushPendingForVersionsList = false;
	
	/**
	 * Constructor used by JPA framework for creating ContentLocale instances.
	 * @since 6.0.0
	 */
	ContentLocale()
	{
	}
	
	/**
	 * Constructor used by {@link Content} while adding a {@link ContentLocale}
	 * in it. The ContentLocale constructed is associated with the given Content.
	 * @param content the content.
	 * @since 6.0.0
	 */
	ContentLocale(Content content)
	{
		this.id = PortalUtil.generateNewGuid();
		this.content = content;
	}
	
	// -- Property Methods -- //
	
	/**
	 * Returns the identifier for this object.
	 * @return the identifier for this object.
	 * @since 4.2.0
	 */
	public String getId()
	{
		return id;
	}
	
	/**
	 * Returns the identifier of the principal that created this object.
	 * @return the principal identifier.
	 * @since 4.2.0
	 */
	public String getCreatedBy()
	{
		return createdBy;
	}
	
	/**
	 * Sets the principal that created this object.
	 * @param createdBy the principal identifier.
	 * @since 4.2.0
	 */
	public void setCreatedBy(String createdBy)
	{
		this.createdBy = createdBy;
	}
	
	/**
	 * Returns the creation date-time of this entry.
	 * @return the creation date-time of this entry.
	 * @since 4.2.0
	 */
	public Date getCreatedDateTime()
	{
		return createdDateTime;
	}
	
	/**
	 * Returns the date-time this object was created.
	 * @param createdDateTime the date-time this object was created.
	 * @since 4.2.0
	 */
	public void setCreatedDateTime(Date createdDateTime)
	{
		this.createdDateTime = createdDateTime;
	}
	
	/**
	 * Returns the locale this object refers to.
	 * @return the locale.
	 * @since 4.2.0
	 */
	public Locale getLocale()
	{
		try
		{
			return LocaleUtil.FromString(locale);
		}
		catch (PortalException e)
		{
			throw new RuntimeException(e);
		}
	}
	
	String getLocaleString()
	{
		return locale;
	}
	
	void setLocaleString(String localeString)
	{
		this.locale = localeString;
	}
	
	/**
	 * Returns the active version of the content in this locale.
	 * @return the active version of the content in this locale.
	 * @since 4.2.0
	 */
	public ContentVersion getActiveVersion()
	{
		return activeVersion;
	}
	
	/**
	 * Returns the published version of the content in this locale.
	 * @return the published version of the content in this locale.
	 * @since 4.2.0
	 */
	public ContentVersion getPublishedVersion()
	{
		return publishedVersion;
	}
	
	/**
	 * Sets the published version of the content in this locale.
	 * @param publishedVersion the published version to set.
	 * @since 4.2.0
	 */
	public void setPublishedVersion(ContentVersion publishedVersion)
	{
		this.publishedVersion = (ContentVersion)publishedVersion;
	}
	
	/**
	 * Returns the comments for the content in this locale. The comments are
	 * ordered chronologically.
	 * @return the comments for the content in this locale.
	 * @since 4.2.0
	 */
	public List<ContentComment> getComments()
	{
		return Collections.unmodifiableList(comments);
	}
	
	/**
	 * Returns the versions for the content in this locale. The versions are
	 * ordered chronologically.
	 * @return the versions for the content in this locale.
	 * @see #addVersion()
	 * @since 4.2.0
	 */
	public List<ContentVersion> getVersions()
	{
		return Collections.unmodifiableList(getVersionsInternal());
	}
	
	/**
	 * Returns the internal {@link #versions} list, flushing if necessary,
	 * according to {@link #flushPendingForVersionsList}.
	 * @return the {@link #versions} list.
	 * @since 6.0.0
	 */
	private List<ContentVersion> getVersionsInternal()
	{
		if (flushPendingForVersionsList)
		{
			ManagerFactory.getEntityManager().flush();
			flushPendingForVersionsList = false;
		}
		return versions;
	}
	
	/**
	 * Returns the tags associated with this {@link ContentLocale}.
	 * 
	 * @return a unmodifiable collection containing {@link ContentTag}s associated with 
	 * this {@link ContentLocale}. The ContentTags returned are the currently active (in case
	 * of multiple versions for a tag).
	 * 
	 * @see #addTag(String)
	 * @see #removeTag(ContentTag)
	 * @since 6.0.0
	 */
	@SuppressWarnings("unchecked")
	Collection<ContentTag> getTags()
	{
		List<ContentTag> resultList = ManagerFactory.getEntityManager().createQuery(
				"select t from ContentTag t inner join t.contentVersion tcv, ContentLocale cl inner join cl.activeVersion acv, ContentLocale currentCL inner join currentCL.tags tags " +
				" where tcv = acv and tags = cl.content and currentCL = :contentLocale")
				.setParameter("contentLocale", this)
				.getResultList();
		
		return Collections.unmodifiableCollection(resultList);
	}
	
	/**
	 * Returns the content this object refers to.
	 * @return the content this object refers to.
	 * @since 4.2.0
	 */
	public Content getContent()
	{
		return content;
	}

	/**
	 * Sets whether this content locale should be verified to be cleaned up or
	 * not.
	 * 
	 * @param dirty
	 *            indicates whether this content locale should be verified to be
	 *            cleaned up or not.
	 * @since 6.0.0
	 */
	void setDirty(boolean dirty)
	{
		this.dirty = dirty;
	}

	// -- Operations -- //
	
	/**
	 * Adds a new comment.
	 * @return the comment added.
	 * @see #getComments()
	 * @since 4.2.0
	 */
	public ContentComment addComment()
	{
		ContentComment comment = new ContentComment();
		comment.setCreatedBy(ContentCoreUtil.getCurrentUserId());
		comment.setCreatedDateTime(new Date());
		comment.setContentLocale(this);
		comments.add(comment);
		return comment;
	}
	
	private ContentVersion addVersion(boolean addPublication)
	{
		ContentVersion version = new ContentVersion();
		version.setLastModifiedBy(ContentCoreUtil.getCurrentUserId());
		version.setLastModifiedDateTime(new Date());
		version.setContentLocale(this);
		if (activeVersion != null)
			version.setVersionNumber(activeVersion.getVersionNumber()+1);
		else
			version.setVersionNumber(1);
		getVersionsInternal().add(version);
		activeVersion = version;
		if (addPublication)
			version.addPublication();

		setDirty(needToBeDirty());
		return version;
	}
	
	/**
	 * Adds a new version. The version added will be made active.
	 * @return the new version added.
	 * @see #getVersions()
	 * @see #getActiveVersion()
	 * @see #addVersionBasedOnAnother(ContentVersion)
	 * @since 4.2.0
	 */
	public ContentVersion addVersion()
	{
		return addVersion(true);
	}

	/**
	 * Returns whether this content locale should be marked as dirty or not.
	 * 
	 * @return true if this content locale should be marked as dirty, false
	 *         otherwise.
	 * @since 6.0.0
	 */
	private boolean needToBeDirty()
	{
		try
		{
			ITransaction transaction = PortalTransactionFactory.getCurrentTransaction();
			String serviceId = ManagerFactory.getServiceInstanceManager().get(getContent().getServiceInstanceId(), transaction).getServiceId();
			Node douiServiceDefinitionNode = DouiManagerFactory.getDouiManager().getDouiServiceDefinitionNode(serviceId, transaction);
			Node versioningNode = XmlUtil.selectSingleNode("/douiDefinition/service/sources/source[@id='" + getContent().getSourceId()
					+ "']/versioning", douiServiceDefinitionNode);

			if (versioningNode == null)
				return false;

			String maxVersions = XmlUtil.readAttributeString("maxVersions", versioningNode);
			if (maxVersions == null || "".equals(maxVersions))
				return false;

			int iMaxVersions = Integer.parseInt(maxVersions);
			return getVersionsInternal().size() > iMaxVersions;
		}
		catch (Exception e)
		{
			logger.error("Exception thrown in needToBeDirty.", e);
			return false;
		}
	}

	/**
	 * Removes a version from this locale.
	 * 
	 * @param version
	 *            the version to be removed.
	 * @since 6.0.0
	 */
	void removeVersion(ContentVersion version)
	{
		// this method is to be called only during version delete.
		// remove from versions to keep consistency only if it is initialized
		if (Hibernate.isInitialized(versions))
		{
			versions.remove(version);
		}
		else
		{
			flushPendingForVersionsList = true;
			
			// clear the cache for ContentLocale.versions 
			PortalCache<?> versionsCache = PortalCache.getCache("lumis.content.core.ContentLocale.versions");
			if (versionsCache != null)
			{
				String cacheEntryKey = "lumis.content.core.ContentLocale.versions#" + getId();
				try
				{
					versionsCache.remove(cacheEntryKey, PortalTransactionFactory.getCurrentTransaction());
				}
				catch (PortalException e)
				{
					logger.error("Exception thrown while evicting ContentLocale.versions for " + cacheEntryKey, e);
					
					// fallback to non-transactional cache eviction 
					versionsCache.remove(cacheEntryKey);
				}
			}
		}
	}

	/**
	 * Adds a new version, based on an already existing version. The version
	 * added will be made active.
	 * 
	 * @param baseVersion
	 *            the version that will be used as a base to create the new
	 *            version to be added.
	 * @return the added version.
	 * @see #addVersion()
	 * @since 4.2.0
	 */
	public ContentVersion addVersionBasedOnAnother(ContentVersion pBaseVersion)
	{
		ContentVersion baseVersion = (ContentVersion)pBaseVersion;
		ContentVersion version = addVersion(false);
		version.setPrimaryName(baseVersion.getPrimaryName());
		version.setIntroduction(baseVersion.getIntroduction());
		Collection<ContentPublication> basePublications = (Collection<ContentPublication>)baseVersion.getPublications();
		for (ContentPublication basePublication: basePublications)
		{
			ContentPublication publication = version.addPublication();
			publication.setHighlight(basePublication.isHighlight());
			publication.setHighlightEndDateTime(basePublication.getHighlightEndDateTime());
			publication.setPublishEndDateTime(basePublication.getPublishEndDateTime());
			publication.setPublishStartDateTime(basePublication.getPublishStartDateTime());
		}
		
		for(ContentAssociation contentAssociation : baseVersion.getContentAssociations())
		{
			ContentAssociation newContentAssociation = version.addContentAssociation();
			newContentAssociation.setAssociatedContent(contentAssociation.getAssociatedContent());
			newContentAssociation.setType(contentAssociation.getType());
		}
		
		return version;
	}
	
	/**
	 * Deletes this instance from persistence. This will also delete all 
	 * content versions that belongs to this locale.
	 * @since 4.2.0
	 */
	public void delete() throws PortalException
	{
		getContent().removeContentLocale(this);
		ManagerFactory.getEntityManager().remove(this);
	}
	
	/**
	 * Moves this content locale object to another content or locale value.
	 * <p>
	 * Deletes this contentLocale's original content, if it becomes empty
	 * after the move is performed. In this case, association references
	 * to that content is updated to refer to contentLocale's new content.
	 * @param newContent a persisted content, to which this content locale is 
	 * to be moved to. If <code>null</code>, this content locale will be
	 * moved to a new content.
	 * @param newLocale the locale to set in this contentLocale.
	 * @throws PortalObjectAlreadyExistsException if this operation would result
	 * in a content having more than one contentLocale with the same locale value.
	 * @since 4.2.0
	 */
	@SuppressWarnings("unchecked")
	public void move(Content newContent, Locale newLocale) throws PortalException
	{
		// if both arguments are null, nothing to do
		if (newLocale == null)
			throw new IllegalArgumentException("locale argument cannot be null");
		
		if (newContent == null)
		{
			newContent = new Content();
			newContent.setServiceInstanceId(getContent().getServiceInstanceId());
			newContent.setSourceId(getContent().getSourceId());
			ContentManagerFactory.getContentManager().addContent(newContent);
		}
		else
		{
			// newContent must be of same service instance and source
			if (newContent != getContent())
			{
				if (!getContent().getServiceInstanceId().equals(newContent.getServiceInstanceId()))
					throw new IllegalArgumentException("Cannot move to a content of another service instance");
				if (!getContent().getSourceId().equals(newContent.getSourceId()))
					throw new IllegalArgumentException("Cannot move to a content of another source");
			}
			
			// check if this move won't cause duplicated locale in a same content
			ContentLocale currentContentLocale = newContent.getContentLocale(newLocale, false);
			if (currentContentLocale != null && currentContentLocale != this)
			{
				String currentContentLocaleId = currentContentLocale.getId();
				if (currentContentLocaleId == null || !currentContentLocaleId.equals(getId()))
					throw new PortalObjectAlreadyExistsException("Content " + newContent.getId() + " already contains locale " + newLocale);
			}
		}
		
		// remove from original content
		Content originalContent = getContent();
		originalContent.removeContentLocale(this);
		
		// set locale
		if (newLocale != null)
			setLocaleString(newLocale.toString());
		
		// add to the new content (even if it is the same original one)
		newContent.addContentLocale(this);
		content = newContent;
		
		// if original content became empty and is persisted
		if (originalContent.getContentLocales().size() == 0 && originalContent.getId() != null)
		{
			// update association references
			List<ContentAssociation> contentAssociations = ManagerFactory.getEntityManager().createQuery(
					"select a from ContentAssociation a where :content = a.associatedContent")
					.setParameter("content", originalContent)
					.getResultList();
			for (ContentAssociation contentAssociation: contentAssociations)
			{
				ContentVersion contentVersion = contentAssociation.getContentVersion();
				
				boolean ignoreThisAssociation = false;
				for(ContentAssociation existingContentAssociation : contentVersion.getContentAssociations())
				{
					if(newContent.getId().equals(existingContentAssociation.getAssociatedContent().getId())
							&& contentAssociation.getType().equals(existingContentAssociation.getType()) )
					{
						ignoreThisAssociation = true;
						break;
					}
				}
				
				if(!ignoreThisAssociation)
				{
					ContentAssociation newContentAssociation = contentVersion.addContentAssociation();
					newContentAssociation.setType(contentAssociation.getType());
					newContentAssociation.setAssociatedContent(newContent);
				}
				
				contentAssociation.delete();
			}
			
			// schedule re-indexing of the original content, because a
			// ContentPublication moved with this ContentLocale may be published
			// to that original content
			ContentManagerFactory.getContentManager().scheduleReindex(originalContent);
			
			// delete the original content
			ContentManagerFactory.getContentManager().deleteContent(originalContent);
		}
		else
		{
			// if originalContent is persisted and newContent refers to different content
			if (originalContent.getId() != null && !originalContent.getId().equals(newContent.getId()))
			{
				// schedule originalContent for indexing (not necessary for 
				// destination content since contentLocale will trigger it)
				ContentManagerFactory.getContentManager().scheduleReindex(originalContent);
			}
		}
	}
	
	// -- Events -- //
	
	@PreUpdate
	@PrePersist
	void validate()
	{
		if (activeVersion == null)
			throw new IllegalStateException("ContentLocale must have an active version");
	}
	
	@PostPersist
	void doPostPersist() throws PortalException
	{
		// TODO: an observer in workflow package should catch a generic 
		// content locale persist event and execute the code below
		
		// creates workflow metadata if the content requires one and it does not
		// exist yet
		IWorkflowManager workflowManager = ContentManagerFactory.getWorkflowManager();
		SessionConfig sessionConfig = SessionConfig.getCurrentSessionConfig();
		ITransaction transaction = PortalTransactionFactory.getCurrentTransaction();
		ContentWorkflowMetaData workflowMetaData = workflowManager.getContentWorkflowMetaData(
				sessionConfig, this, transaction);
		if (workflowMetaData == null)
		{
			workflowManager.createNewContentWorkflowMetaData(sessionConfig, this, 
					sessionConfig.getUserId(), transaction);
		}
	}
	
	@PreRemove
	void doPreRemove() throws PortalException
	{
		ContentManagerFactory.getWorkflowManager().deleteContentWorkflowMetaData(
				SessionConfig.getCurrentSessionConfig(), this, 
				PortalTransactionFactory.getCurrentTransaction());

		// if removing a content locale, the <item id, content locale id>
		// mapping shouldn't be sent over the cluster.
		for (ContentVersion version : getVersions())
			version.sendDeleteClusterCommand = false;
	}

	/**
	 * Returns the lock for the current content-locale.
	 * @return the current lock for this content-locale.
	 * @since 6.0.0
	 */
	public LockType getLockType()
	{
		if(lockType == null)
		{
			return LockType.NONE;
		}
		return lockType;
	}
	
	/**
	 * Sets the locktype for the current contentLocale.  This method should only be used by the IContentManager implementation.
	 * @param lockType the new state of the contentLocale. 
	 * @since 6.0.0
	 */
	void setLockType(LockType lockType)
	{
		this.lockType = lockType;
	}

	/**
	 * Returns who created the lock.
	 * @return the lockedBy userId of the holder of the current lock
	 * @since 6.0.0
	 */
	public String getLockedBy()
	{
		return lockedBy;
	}
	
	/**
	 * Sets who locked the current contentLocale.  This method should only be used by the IContentManager implementation.
	 * @param lockedBy the publisher who locked the contentLocale
	 * @since 6.0.0
	 */
	void setLockedBy(String lockedBy)
	{
		this.lockedBy = lockedBy;
	}

	/**
	 * Returns the date the lock was created.
	 * @return the lockedDate date of the lock creation date/time.
	 * @since 6.0.0
	 */
	public Date getLockedDate()
	{
		return lockedDate;
	}

	/**
	 * Set the time that the lock occurred, if the contentLocale was locked.  This method should only be used by the IContentManager implementation.
	 * @param lockedDate the time the lock occurred, if there is any.
	 * @since 6.0.0
	 */
	void setLockedDate(Date lockedDate)
	{
		this.lockedDate = lockedDate;
	}

	/**
	 * Associates a tag to this {@link ContentLocale}. Returns true if the tag was associated 
	 * to this {@link ContentLocale}, otherwise return false. 
	 * <p>
	 * If the given tag name corresponds to a tag's active content version in the 
	 * same tag repository, that tag is used for the association. Otherwise a new tag is 
	 * created for the association.
	 * <p>
	 * The tag name comparison is case insensitive.
	 * <p>
	 * The max length of a tag name is 30 characters.  
	 * 
	 * @param tag the tag name.
	 * @return true if the tag was associated to this {@link ContentLocale}, false otherwise.
	 * @throws IllegalArgumentException if the given tag name is not valid.
	 * @throws IllegalStateException if the tag repository is not associated to the content repository.
	 * @see #removeTag(ContentTag)
	 * @see #getTags()
	 * @since 6.0.0
	 */
	boolean addTag(String tag)
	{
		// verifies tag length
		if(tag == null || tag.length() == 0)
			throw new IllegalArgumentException("Tag name length must be bigger than zero.");
			
		// discover the content tag repository associated to this content
		String serviceInstanceId = this.getContent().getServiceInstanceId();
		ServiceInstanceConfig serviceInstanceTag;
		try 
		{
			serviceInstanceTag = ManagerFactory.getServiceInstanceManager().getServiceInstanceByDependency(
					SessionConfig.getCurrentSessionConfig(), serviceInstanceId, ServiceInstanceDependencyConfig.DEPENDENCY_TYPE_TAG_REPOSITORY, 
					PortalTransactionFactory.getCurrentTransaction());
		}
		catch (PortalException e) 
		{
			throw new RuntimeException(e);
		}

		if(serviceInstanceTag == null)
			throw new IllegalStateException("Tag repository is required to tag contents.");
		
		Content content = getContentByTagName(tag, serviceInstanceTag.getId());
		
		boolean isAssociated = false;
		// add in the association collection
		if(!tags.contains(content))
		{
			isAssociated = tags.add(content);
		}
		
		return isAssociated;
	}
	
	/**
	 * Returns the {@link Content} for the given tag name.
	 * <p>
	 * Returns an existing content if the given tag name already exists in a active version
	 * in the given service instance, else a new tag is created. 
	 * 
	 * @param tag the given tag name.
	 * @param serviceInstanceId the identifier of service instance of tag
	 * @return the content for a given tag name and service instance
	 * @since 6.0.0
	 */
	private Content getContentByTagName(String tag, String serviceInstanceId)
	{
		Content content;
		try
		{
			content = getExistentContentByTagName(tag, serviceInstanceId);
		}
		catch (NoResultException e) 
		{
			content = createTag(tag, serviceInstanceId);
		}
			
		return content;
	}
	
	/**
	 * Returns an existing content for a given tag name and given service instance.
	 * 
	 * @param tag the tag name.
	 * @param serviceInstanceId the identifier of service instance of tag.
	 * @return the existent content for the given tag name and service instance.
	 * @since 6.0.0
	 */
	private Content getExistentContentByTagName(String tag, String serviceInstanceId) 
	{
		return (Content) ManagerFactory.getEntityManager().createQuery(
				"select c from ContentTag t inner join t.contentVersion tcv, ContentLocale cl " +
				"inner join cl.content c where tcv = cl.activeVersion and c.serviceInstanceId = :serviceInstanceId and t.normalizedName = :normalizedName")
				.setParameter("normalizedName", ContentUtil.normalizeTagName(tag))
				.setParameter("serviceInstanceId", serviceInstanceId)
				.getSingleResult();
	}
	
	/**
	 * Returns the content of the new {@link ContentTag}.
	 * <p>
	 * Creates a new {@link ContentTag} using the given Tag name into a given service instance.
	 * 
	 * @param tag the tag name
	 * @param serviceInstanceId the service instance identifier
	 * @return the content of the new {@link ContentTag}
	 * @since 6.0.0
	 * @see ContentTag#setName(String)
	 */
	private Content createTag(String tag, String serviceInstanceId)
	{
		// add tag entry
		ContentTag contentTag = new ContentTag();
		contentTag.setName(tag);
		ManagerFactory.getEntityManager().persist(contentTag);
		
		// create content for tag
		Content content = new Content();
		content.setServiceInstanceId(serviceInstanceId);
		content.setSourceId(ContentTag.SOURCE_ID);
		Locale defaultLocale = SessionConfig.getCurrentSessionConfig().getLocale();
		content.setDefaultLocale(defaultLocale);
		try
		{
			// add locale
			ContentLocale contentLocale = content.addContentLocale(defaultLocale);
			// add version
			ContentVersion contentVersion = contentLocale.addVersion();
			contentVersion.setItemId(contentTag.getId());
			contentVersion.setPrimaryName(contentTag.getName());
			// add content
			ContentManagerFactory.getContentManager().addContent(content);
			// sets content version in contentTag to avoid a new read.
			contentTag.setContentVersion(contentVersion);
		}
		catch (PortalException e) 
		{
			throw new RuntimeException(e);
		}
		
		return content;
	}
	
	/**
	 * Removes the association between this {@link ContentLocale} and the given {@link ContentTag}.
	 * 
	 * @param contentTag, the content tag.
	 * @return true if the tag was dissociated, false otherwise.
	 * @see #addTag(String)
	 * @see #getTags()
	 * @since 6.0.0
	 */
	boolean removeTag(ContentTag contentTag)
	{
		if(contentTag == null)
			throw new IllegalArgumentException("Parameter null is not valid.");
		
		return tags.remove(contentTag.getContentVersion().getContentLocale().getContent());
	}
}